#include "EnvironmentMap.h"

EnvironmentMap::EnvironmentMap(const char* prefix, const char* ext) {
    string extString(ext);

    FREE_IMAGE_FORMAT format;
    if (extString == ".tga") {
        //special .tga handling?
        return;
    } 
    
    if (extString == ".jpg" || extString == ".jpeg") format = FIF_JPEG;
    else if (extString == ".png") format = FIF_PNG;
    else format = FIF_UNKNOWN;

    glGenTextures(1, &cubeMap);
    glBindTexture(GL_TEXTURE_CUBE_MAP, cubeMap);

    glGenTextures(6, cubeTextures);
    glGenTextures(6, skyboxTextures);
    for (int i = 0; i < 6; i++) {
        string filename = prefix + faceNames[i] + extString;
        FIBITMAP* rawBitmap = FreeImage_Load(format, filename.c_str());

        if (!rawBitmap) {
            printf("Error reading environment map %s", filename.c_str());
            return;
        }

        size = FreeImage_GetWidth(rawBitmap);
        FIBITMAP* bitmap32 = FreeImage_ConvertTo32Bits(rawBitmap);
        FreeImage_Unload(rawBitmap);

        if (!bitmap32) {
            printf("Error reading environment map %s", filename.c_str());
            return;
        }

        unsigned char* pixels = (unsigned char*) FreeImage_GetBits(bitmap32);

        data[i] = new GLubyte[4 * size * size];

        //flip texture
        for (int j = 0; j < size; j++) {
            for (int k = 0; k < size; k++) {
                int sourceBase = 4 * ((size - j - 1) * size + k);
                int targetBase = 4 * (j * size + k);
                data[i][targetBase+0] = pixels[sourceBase+2];
		        data[i][targetBase+1] = pixels[sourceBase+1];
		        data[i][targetBase+2] = pixels[sourceBase+0];
		        data[i][targetBase+3] = pixels[sourceBase+3];
            }
        }

        FreeImage_Unload(bitmap32);

        glTexImage2D(faceTargets[i], 0, GL_RGBA, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, data[i]);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glBindTexture(GL_TEXTURE_2D, skyboxTextures[i]);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, data[i]);
    }

    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}

EnvironmentMap::EnvironmentMap(int size, GLubyte* data[]) {
    this->size = size;

    glGenTextures(1, &cubeMap);
    glBindTexture(GL_TEXTURE_CUBE_MAP, cubeMap);

    glGenTextures(6, cubeTextures);
    glGenTextures(6, skyboxTextures);

    for (int i = 0; i < 6; i++) {
        this->data[i] = data[i];

        glTexImage2D(faceTargets[i], 0, GL_RGBA, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, data[i]);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glBindTexture(GL_TEXTURE_2D, skyboxTextures[i]);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size, size, 0, GL_RGBA, GL_UNSIGNED_BYTE, data[i]);
    }

    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}

void EnvironmentMap::setCubeMap() {
    glBindTexture(GL_TEXTURE_CUBE_MAP, cubeMap);
    for (int i = 0; i < 6; i++) {
        glBindTexture(faceTargets[i], cubeTextures[i]);
    }
}

void EnvironmentMap::drawSkybox(GLfloat size) {
    glEnable(GL_TEXTURE_2D);
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, skyboxTextures[0]);

    glPushMatrix();
        //glScalef(size, size, size);
        //glColor3f(1.0, 0.0, 0.0);
        glBegin(GL_QUADS);
            glNormal3f(-1.0, 0.0, 0.0);
            glTexCoord2f(0.0, 0.0);
            glVertex3f(1.0, -1.0, -1.0);
            glTexCoord2f(1.0, 0.0);
            glVertex3f(1.0, -1.0, 1.0);
            glTexCoord2f(1.0, 1.0);
            glVertex3f(1.0, 1.0, 1.0);
            glTexCoord2f(0.0, 1.0);
            glVertex3f(1.0, 1.0, -1.0);
        glEnd();
    glPopMatrix();

    glDisable(GL_TEXTURE_2D);
}

void EnvironmentMap::integrate(GLfloat& r, GLfloat& g, GLfloat& b, cubemapBRDF f, GLfloat vx, GLfloat vy, GLfloat vz, int step) const {
    r = 0.0;
    g = 0.0;
    b = 0.0;
    GLfloat w = 0.0;

    for (int i = 0; i < 6; i++) {
        for (int pt = 0; pt < size; pt += step) {
            for (int ps = 0; ps < size; ps += step) {
                int base = 4 * (pt * size + ps);

                float s = (ps + 0.5) / size;
                float t = (pt + 0.5) / size;
                float ix, iy, iz;
                cubemapXYZFromST(ix, iy, iz, i, s, t);

                float weight = f(ix, iy, iz, vx, vy, vz);
                r += data[i][base+0] * weight;
                g += data[i][base+1] * weight;
                b += data[i][base+2] * weight;
                w += weight;
            }
        }
    }

    r /= w;
    g /= w;
    b /= w;
}

void EnvironmentMap::integrate(GLfloat& r, GLfloat& g, GLfloat& b, cubemapWeight f) const {
    r = 0.0;
    g = 0.0;
    b = 0.0;

    for (int i = 0; i < 6; i++) {
        for (int pt = 0; pt < size; pt++) {
            for (int ps = 0; ps < size; ps ++) {
                int base = 4 * (pt * size + ps);

                float s = (ps + 0.5) / size;
                float t = (pt + 0.5) / size;
                float ix, iy, iz;
                cubemapXYZFromST(ix, iy, iz, i, s, t);

                float weight = f(ix, iy, iz);
                r += data[i][base+0] * weight;
                g += data[i][base+1] * weight;
                b += data[i][base+2] * weight;
            }
        }
    }

    r *= 4.0 / (size * size * pi);
    g *= 4.0 / (size * size * pi);
    b *= 4.0 / (size * size * pi);
}

EnvironmentMap* EnvironmentMap::generateDiffuseMap(int size, int step) const {
    GLubyte** diffuseData = new GLubyte* [6];
    
    for (int i = 0; i < 6; i++) {
        diffuseData[i] = new GLubyte[4 * size * size];
        for (int pt = 0; pt < size; pt++) {
            for (int ps = 0; ps < size; ps++) {
                int base = 4 * (pt * size + ps);
                float s = (ps + 0.5) / size;
                float t = (pt + 0.5) / size;
                float vx, vy, vz;
                cubemapXYZFromST(vx, vy, vz, i, s, t);

                float r, g, b;
                integrate(r, g, b, lambertian, vx, vy, vz, step);
                diffuseData[i][base+0] = GLuint(r);
                diffuseData[i][base+1] = GLuint(g);
                diffuseData[i][base+2] = GLuint(b);
                diffuseData[i][base+3] = GLuint(255);
            }
        }
    }
    return new EnvironmentMap(size, diffuseData);
}

EnvironmentMap* EnvironmentMap::generateDiffuseMapY(int size) const {
    float** floatData = new float* [6];
    GLubyte** diffuseData = new GLubyte* [6];

    for (int i = 0; i < 6; i++) {
        floatData[i] = new float[4 * size * size];
        diffuseData[i] = new GLubyte[4 * size * size];
        for (int pt = 0; pt < size; pt++) {
            for (int ps = 0; ps < size; ps++) {
                int base = 4 * (pt * size + ps);
                floatData[i][base+0] = 0;
                floatData[i][base+1] = 0;
                floatData[i][base+2] = 0;
                //ignore alpha
            }
        }
    }

    for (int fi = 0; fi < 9; fi++) {
        cubemapWeight* fy;
        cubemapWeight* fye;
        switch (fi) {
            case 0: fy = &y00; fye = &y00e; break;
            case 1: fy = &y11; fye = &y11e; break;
            case 2: fy = &y10; fye = &y10e; break;
            case 3: fy = &y1n1; fye = &y1n1e; break;
            case 4: fy = &y22; fye = &y22e; break;
            case 5: fy = &y21; fye = &y21e; break;
            case 6: fy = &y20; fye = &y20e; break;
            case 7: fy = &y2n1; fye = &y2n1e; break;
            case 8: fy = &y2n2; fye = &y2n2e; break;
        }

        float r, g, b;
        integrate(r, g, b, *fy);

        for (int i = 0; i < 6; i++) {
            for (int pt = 0; pt < size; pt++) {
                for (int ps = 0; ps < size; ps++) {
                    int base = 4 * (pt * size + ps);
                    float s = (ps + 0.5) / size;
                    float t = (pt + 0.5) / size;
                    float vx, vy, vz;
                    cubemapXYZFromST(vx, vy, vz, i, s, t);
                    
                    float vc = (*fye)(vx, vy, vz);
                    floatData[i][base+0] += r * vc;
                    floatData[i][base+1] += g * vc;
                    floatData[i][base+2] += b * vc;
                }
            }
        }
    }

    for (int i = 0; i < 6; i++) {
        for (int pt = 0; pt < size; pt++) {
            for (int ps = 0; ps < size; ps++) {
                int base = 4 * (pt * size + ps);
                int r = floatData[i][base+0];
                int g = floatData[i][base+1];
                int b = floatData[i][base+2];

                if (r > 255) r = 255;
                else if (r < 0) r = 0;
                if (g > 255) g = 255;
                else if (g < 0) g = 0;
                if (b > 255) b = 255;
                else if (b < 0) b = 0;

                diffuseData[i][base+0] = GLubyte(r);
                diffuseData[i][base+1] = GLubyte(g);
                diffuseData[i][base+2] = GLubyte(b);
                diffuseData[i][base+3] = GLubyte(255);
            }
        }
    }

    delete[] floatData;

    return new EnvironmentMap(size, diffuseData);
}
